/**
 * Copyright (c) 2008 Trent Hoeppner
 * 
 * This file is part of BillCalc.
 * 
 * BillCalc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * BillCalc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with BillCalc.  If not, see <http://www.gnu.org/licenses/>.
 **/

package com.trentech.billcalc;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import testcases.MainTest;

/**
 * Resident represents a person who lives, or has lived in the apartment.
 * 
 * @author Trent Hoeppner
 */
public class Resident extends Entity {
    
    /**
     * Loan is the Biller that is used to track what Residents owe each other,
     * but which has no category associated with a real Biller.
     */
    private final static Biller LOAN_BILLER = new Biller("Loan", new Date());
    

    /**
     * The date when the person arrived.
     */
    private Date arrivalDate;
    
    
    /**
     * The date when the person left the apartment.  May be null if the person
     * is still living in the apartment.
     */
    private Date departureDate;
    
    
    /**
     * A mapping from Billers to how much is owed to that Biller by this.
     */
    private Map<Biller, BigDecimal> billerToOwedMap = new HashMap<Biller, BigDecimal>();
    
    
    /**
     * A mapping from Billers to times when this Resident has opted out of
     * paying the bill.
     */
    private Map<Biller, DateGroup> billerToOptOutRangeMap = new HashMap<Biller, DateGroup>();
    
    
    /**
     * A range of dates when this has gone on vacation.
     */
    private DateGroup vacationRange = new DateGroup();
    
    
    /**
     * A mapping from Residents to what this owes that Resident, broken down 
     * into Billers.
     */
    private Map<Resident, Map<Biller, BigDecimal>> roommateToOriginalDebtMap = 
        new HashMap<Resident, Map<Biller, BigDecimal>>(); 
    
    
    /**
     * Constructor for Resident.
     * 
     * @param   name        the name of the resident.  Cannot be null or empty.
     * @param   arrivalDate the date that the person moved in to the apartment.  
     *                      Cannot be null.
     */
    public Resident(String name, Date arrivalDate) {
        super(name);
        this.arrivalDate = arrivalDate;
    }


    /**
     * Adjusts what this owes to the given biller by the given amount.  If the
     * amount is positive, the total owed to the biller will be increased, 
     * otherwise it will be decreased.
     * 
     * @param   biller  the name of the biller that this owes.  Cannot be null 
     *                  or empty.
     * @param   amount  the amount to add to the total owed to the given biller.
     */
    public void charge(Biller biller, BigDecimal amount) {
        BigDecimal owed = billerToOwedMap.get(biller);
        
        // original : 
        // if (owed == null) {
        // seed fault : 
        if (MainTest.isOriginal || MainTest.isFaultFound ? (owed == null) : (owed != null)) {
        	
            owed = BigDecimal.ZERO;
        }
        owed = owed.add(amount);
        billerToOwedMap.put(biller, owed);
        
        if (owed.compareTo(BigDecimal.ZERO) > 0) {
            System.out.println(getName() + " should pay " + biller.getName() + " " + owed);
        }
        else if (owed.compareTo(BigDecimal.ZERO) < 0) {
            System.out.println(biller.getName() + " should pay " + getName() + " " + owed.negate());
        }
    }


    /**
     * Adjusts what this owes to the given Resident by the given amount.  If the
     * amount is positive, the total owed to the Resident will be increased, 
     * otherwise it will be decreased.
     * 
     * @param   resident    the name of the Resident that this owes.  Cannot be 
     *                      null or empty.
     * @param   amount      the amount to add to the total owed to the given 
     *                      Resident.
     */
    public void charge(Resident resident, BigDecimal amount) {
        Map<Biller, BigDecimal> originalDebtMap = getOriginalDebtMap(resident);

        // put the amount as a loan
        BigDecimal loan = originalDebtMap.get(LOAN_BILLER);
        if (loan == null) {
            loan = BigDecimal.ZERO;
        }
        
        loan = loan.add(amount);
        originalDebtMap.put(LOAN_BILLER, loan);

        // adjust the loan so that other debts/credits are paid
        resolveLoan(resident);
    }


    /**
     * Uses the Loan amount to pay off other debts to the given Resident.  If
     * this Resident borrowed money from the given Resident (the Loan is 
     * positive), and the given Resident owes this Resident money for other 
     * bills (the original debts toward the given Resident are negative), then 
     * this Resident does not have to pay back some or all of the Loan.  This 
     * also works in reverse (a negative Loan and positive bills).
     * 
     * @param   resident    the Resident to resolve the loan for.  Cannot be 
     *                      null.
     */
    private void resolveLoan(Resident resident) {
        Map<Biller, BigDecimal> originalDebtMap = getOriginalDebtMap(resident);
        BigDecimal loan = originalDebtMap.get(LOAN_BILLER);
        if (loan == null) {
            loan = BigDecimal.ZERO;
        }
        
        // reducing the debt to the other resident pays off each bill in turn
        boolean loanPositive = gtE0(loan);
        
        Iterator<Biller> billerIterator = originalDebtMap.keySet().iterator();
        while (billerIterator.hasNext() && loan.compareTo(BigDecimal.ZERO) != 0) {
            Biller biller = billerIterator.next();
            BigDecimal owed = originalDebtMap.get(biller);
            boolean owedPositive = gtE0(owed);
            if (owedPositive != loanPositive) {
                
                // one can be used to pay off the other
                
                if (owed.abs().compareTo(loan.abs()) <= 0) {
                    // the amount owed is smaller than the loan
                    // the resident can completely pay off this bill, and 
                    // maybe part of another
                    loan = loan.add(owed);
                    originalDebtMap.put(biller, BigDecimal.ZERO);
                }
                else {
                    // the resident can pay a portion of this bill
                    owed = owed.add(loan);
                    originalDebtMap.put(biller, owed);
                    loan = BigDecimal.ZERO;
                }
            }
        }
        
        originalDebtMap.put(LOAN_BILLER, loan);
    }
    
    
    /**
     * Returns whether the given number is greater than or equal to zero.
     * 
     * @param   num the number to test.  Cannot be null.
     * 
     * @return  true if the number is greater than or equal to zero, false 
     *          otherwise.
     */
    private boolean gtE0(BigDecimal num) {
        return num.compareTo(BigDecimal.ZERO) >= 0;
    }
    
    
    /**
     * Transfers the given amount of debt from the given Biller to the given 
     * Resident.  This method should be used when one Resident pays a bill on
     * behalf of other Residents, and then needs to be paid back by those 
     * Residents.
     * 
     * @param   biller      the biller who charged the original money to the 
     *                      household.  Cannot be null.
     * @param   resident    the Resident who paid the bill on behalf of this 
     *                      Resident.  Cannot be null.
     * @param   amount      the amount of debt to transfer.  A positive number 
     *                      indicates that debt will be reduced toward the 
     *                      biller, and increased toward the resident.  Cannot 
     *                      be null.
     */
    public void transferDebt(Biller biller, Resident resident, BigDecimal amount) {
        BigDecimal owed = billerToOwedMap.get(biller);
        if (owed == null) {
            owed = BigDecimal.ZERO;
        }
        owed = owed.subtract(amount);
        billerToOwedMap.put(biller, owed);

        Map<Biller, BigDecimal> originalDebtMap = getOriginalDebtMap(resident);
        
        BigDecimal originalOwed = originalDebtMap.get(biller);
        if (originalOwed == null) {
            originalOwed = BigDecimal.ZERO;
        }
        originalOwed = originalOwed.add(amount);
        originalDebtMap.put(biller, originalOwed);
        
        resolveLoan(resident);
    }
    
    
    /**
     * Adjusts what this owes to the given Resident in the given category by
     * the given amount.  This is used for non-shared bills like rent, deposit, 
     * etc.
     * 
     * @param   biller      the category that the charge belongs to (e.g. 
     *                      Rent).  Cannot be null or empty.
     * @param   resident    the Resident that this owes.  Cannot be null.
     * @param   amount      the amount to adjust what this owes the given 
     *                      resident by.  Cannot be null.
     */
    public void chargeSpecificBill(Biller biller, Resident resident, BigDecimal amount) {
        Map<Biller, BigDecimal> originalDebtMap = getOriginalDebtMap(resident);
        
        BigDecimal originalOwed = originalDebtMap.get(biller);
        if (originalOwed == null) {
            originalOwed = BigDecimal.ZERO;
        }
        originalOwed = originalOwed.add(amount);
        originalDebtMap.put(biller, originalOwed);
        
        resolveLoan(resident);
    }


    /**
     * Returns the mapping from the original Billers to the amount that should 
     * have been paid to them, but which was paid by the given Resident.
     * 
     * @param   resident    the Resident with whom this Resident has debts or 
     *                      credits with.  Cannot be null.
     * 
     * @return  the mapping from original Billers to amounts owed to them for
     *          the given Resident.  Will not be null.
     */
    private Map<Biller, BigDecimal> getOriginalDebtMap(Resident resident) {
        Map<Biller, BigDecimal> originalDebtMap = roommateToOriginalDebtMap.get(resident);
        if (originalDebtMap == null) {
            originalDebtMap = new HashMap<Biller, BigDecimal>();
            roommateToOriginalDebtMap.put(resident, originalDebtMap);
        }
        return originalDebtMap;
    }
    
    
    /**
     * Returns whether this should pay money to the given biller.
     * 
     * @param   biller  the name of the biller to check.  Cannot be null or 
     *                  empty.
     *                  
     * @return  true if this should pay the biller, false otherwise.
     */
    public boolean owes(Biller biller) {
    	return billerToOwedMap.containsKey(biller) 
            && billerToOwedMap.get(biller).compareTo(BigDecimal.ZERO) > 0;
    }
    
    
    /**
     * Returns the Billers that this has unresolved payments with.
     * 
     * @return  the list of Billers.  Will not be null.
     */
    public List<Biller> getOwedBillers() {
        return new ArrayList<Biller>(billerToOwedMap.keySet());
    }


    /**
     * Returns the amount owed to the given biller.  A negative amount 
     * indicates that the biller should pay this.  
     * 
     * @param   biller  the name of the biller to check.  Cannot be null or 
     *                  empty.
     *                  
     * @return  the amount that this person should pay to the given biller.  
     *          Will not be null.
     */
    public BigDecimal getAmountOwed(Biller biller) {
        return billerToOwedMap.get(biller);
    }
    
    
    /**
     * Mark a start date when this Resident opts out of paying the given 
     * Biller.
     * 
     * @param   biller  the Biller to stop paying.  Cannot be null.
     * @param   date    the day to stop paying.  Cannot be null.
     */
    public void optOutStart(Biller biller, Date date) {
        DateGroup optOutDates = billerToOptOutRangeMap.get(biller);
        if (optOutDates == null) {
            optOutDates = new DateGroup();
            billerToOptOutRangeMap.put(biller, optOutDates);
        }
        
        optOutDates.startRange(date);
    }


    /**
     * Mark a date when this Resident restarts paying the given Biller.
     * 
     * @param   biller  the Biller to start paying.  Cannot be null.
     * @param   date    the day to start paying.  Cannot be null.
     */
    public void optOutEnd(Biller biller, Date date) {
        DateGroup optOutDates = billerToOptOutRangeMap.get(biller);
        optOutDates.endRange(date);
    }
    
    
    /**
     * Returns whether this has opted out of paying the given Biller on the 
     * given day.
     * 
     * @param   biller  the Biller that may or may not be paid by this.  Cannot 
     *                  be null.
     * @param   date    the day to ask about.  Cannot be null.
     * 
     * @return  true if this should NOT pay the Biller, false otherwise.
     */
    public boolean isOptedOut(Biller biller, Date date) {
        boolean optedOut = false;
        DateGroup optOutDates = billerToOptOutRangeMap.get(biller);
        if (optOutDates != null && optOutDates.contains(date)) {
            optedOut = true;
        }
        
        return optedOut;
    }


    /**
     * Mark a start date when this Resident goes on vacation.
     * 
     * @param   date    the first day of not being in the apartment.  Cannot be 
     *                  null.
     */
    public void vacationStart(Date date) {
        vacationRange.startRange(date);
    }


    /**
     * Mark a date when this Resident comes back from vacation.
     * 
     * @param   date    the day this returned to the apartment.  Cannot be
     *                  null.
     */
    public void vacationEnd(Date date) {
        vacationRange.endRange(date);
    }
    
    
    /**
     * Returns whether this is on vacation on the given day.
     * 
     * @param   date    the day to ask about.  Cannot be null.
     * 
     * @return  true if this is on vacation, false otherwise.
     */
    public boolean isOnVacation(Date date) {
        boolean onVacation = vacationRange.contains(date);
        return onVacation;
    }


    /**
     * Prints a report to stdout of how much this owes to each biller so far.
     * 
     * @param   manager the FinanceManager to help determine the costs for 
     *                  this.  Cannot be null.
     */
    public void report(FinanceManager manager) {
        System.out.println(getName());
        Iterator<Biller> billerIterator = billerToOwedMap.keySet().iterator();
        while (billerIterator.hasNext()) {
            Biller biller = billerIterator.next();
            BigDecimal owed = billerToOwedMap.get(biller);
            System.out.print("\t" + biller + "\t" + owed);
            
            if (biller != LOAN_BILLER && manager.hasEstimatedCosts(this, biller)) {
                BigDecimal estimate = manager.calcEstimatedCost(this, biller);
                System.out.print(" <" + estimate + ">");
            }
            System.out.println();
        }
        
        Iterator<Resident> residentIterator = roommateToOriginalDebtMap.keySet().iterator();
        while (residentIterator.hasNext()) {
            Resident resident = residentIterator.next();
            Map<Biller, BigDecimal> originalDebtMap = roommateToOriginalDebtMap.get(resident);
            System.out.print("\t" + resident + " (");
            
            BigDecimal total = BigDecimal.ZERO;
            billerIterator = originalDebtMap.keySet().iterator();
            while (billerIterator.hasNext()) {
                Biller biller = billerIterator.next();
                BigDecimal owed = originalDebtMap.get(biller);
                total = total.add(owed);
                System.out.print("\t" + biller + ": " + owed);
            }

            System.out.print(") Total: " + total);

            System.out.println();
        }
    }


    /**
     * Returns the date that this moved in to the apartment.
     * 
     * @return  the date that this moved in.  Will not be null.
     */
    public Date getArrivalDate() {
        return arrivalDate;
    }


    /**
     * Returns the date that this moved out of the apartment.
     * 
     * @return  the date that this moved out.  A null value indicates that this
     *          is still living in the apartment.
     * 
     */
    public Date getDepartureDate() {
        return departureDate;
    }
    
    
    /**
     * Sets the date that this moved out of the apartment.
     * 
     * @param   date    the date that this moved out.  Cannot be null, and must
     *                  be after the {@link #getArrivalDate() arrival date}.
     */
    public void setDepartureDate(Date date) {
        this.departureDate = date;
    }
    
    
    /**
     * Returns the Residents that this owes.
     * 
     * @return  the list of Residents that this owes.  Will not be null.
     */
    public List<Resident> getOwedResidents() {
        return new ArrayList<Resident>(roommateToOriginalDebtMap.keySet());
    }
    
    
    /**
     * Returns the amount owed by this toward the given Resident, broken down 
     * by Biller.
     * 
     * @param   resident    the Resident that this should pay.  Cannot be null.
     * 
     * @return  a mapping from Billers to the amounts owed toward each Biller.
     *          Will be null if this does not owe the given Resident.
     */
    public Map<Biller, BigDecimal> getDebtsTowardResident(Resident resident) {
        return roommateToOriginalDebtMap.get(resident);
    }
}



